#include "wxvbam.h"
#include "viewsupt.h"
#include "../common/ConfigManager.h"

namespace Viewers
{
void Viewer::CloseDlg(wxCloseEvent &ev)
{
	// stop tracking dialog
	MainFrame* f = wxGetApp().frame;

	for (dialog_list_t::iterator i = f->popups.begin();
	        i != f->popups.end(); i++)
		if (*i == this)
		{
			f->popups.erase(i);
			break;
		}

	// just forwarding this event does not cause system to close window
	// ev.Skip();
	// so do it manually
	Destroy();
}

Viewer::Viewer(const wxChar* name) : wxDialog(), auto_update(false)
{
	dname = name;
	MainFrame* f = wxGetApp().frame;

	// using LoadDialog precludes non-wxDialog nodes, which is good
	// since Viewer is strictly wxDialog-derived.
	if (!wxXmlResource::Get()->LoadDialog(this, f, name))
		baddialog();

	f->popups.push_back(this);
	SetEscapeId(wxID_CLOSE);
	Fit();
}

void Viewer::ActiveCtrl(wxCommandEvent &ev)
{
	wxWindow* ctrl = wxStaticCast(ev.GetEventObject(), wxWindow);
	// TransferDataFromWindow() only operates on children, not self
	// so just simulate
	wxValidator* v = ctrl->GetValidator();

	if (v)
	{
		v->TransferFromWindow();
		Update();
	}
}

BEGIN_EVENT_TABLE(Viewer, wxDialog)
	EVT_CLOSE(Viewer::CloseDlg)
	EVT_CHECKBOX(XRCID("AutoUpdate"), Viewer::SetAutoUpdate)
	EVT_RADIOBUTTON(wxID_ANY, Viewer::ActiveCtrl)
	EVT_SPINCTRL(wxID_ANY, Viewer::ActiveCtrlSpin)
	// this does not interfere with/intercept disassemble scrolls
	EVT_SCROLL(Viewer::ActiveCtrlScr)
END_EVENT_TABLE()
}

void MainFrame::UpdateViewers()
{
	for (dialog_list_t::iterator i = popups.begin(); i != popups.end(); i++)
	{
		Viewers::Viewer* d = static_cast<Viewers::Viewer*>(*i);

		if (d->auto_update)
			d->Update();
	}
}

namespace Viewers
{
IMPLEMENT_DYNAMIC_CLASS(DisList, wxPanel)

DisList::DisList() :
	wxPanel(), tc(), sb(), didinit(false), nlines(0), issel(false),
	topaddr(0)
{
}

void DisList::Refit(int cols)
{
	if (!didinit)
	{
		tc.Create(this, wxID_ANY, wxEmptyString, wxPoint(0, 0), wxDefaultSize,
		          wxTE_READONLY | wxTE_MULTILINE | wxTE_DONTWRAP |
		          wxTE_NOHIDESEL);
		tc.Connect(wxEVT_LEFT_DOWN, wxMouseEventHandler(DisList::MouseEvent),
		           NULL, this);
		tc.Connect(wxEVT_LEFT_UP, wxMouseEventHandler(DisList::MouseEvent),
		           NULL, this);
		tc.Connect(wxEVT_MOTION, wxMouseEventHandler(DisList::MouseEvent),
		           NULL, this);
		// FIXME: take tc out of tab order
		sb.Create(this, wxID_ANY, wxDefaultPosition, wxDefaultSize,
		          wxSB_VERTICAL);
		sb.SetScrollbar(0, 15, 500, 15);
		didinit = true;
	}

	wxString line(wxT('M'), cols);
	// GetBestSize() in wxGTK is pretty damn worthless.  It's surprising
	// autosizing works at all.  wxGTK does not give good values
	// for wxTextCtrl (always 80xht_of_1liner)
	// Given this, we'll fake it using wxStaticText for lineheight
	// and add an extra line for extraheight
	// Just using GetTextExtent for lineheight might not work if extra
	// spacing is usually added between lines.
	wxStaticText st(this, wxID_ANY, line);
	wxSize winsz = st.GetBestSize();
	st.SetLabel(line + wxT('\n') + line);
	wxSize winsz2 = st.GetBestSize();
	lineheight = winsz2.GetHeight() - winsz.GetHeight();
	extraheight = winsz.GetHeight() /* - lineheight */;
	winsz.SetHeight(extraheight + 15 * lineheight);
	winsz.SetWidth(winsz.GetWidth() + sb.GetBestSize().GetWidth());
	SetMinSize(winsz);
	SetSize(winsz);
}

void DisList::MoveSB()
{
	int pos;

	if (topaddr <= 100)
		pos = topaddr;
	else if (topaddr >= maxaddr - 100)
		pos = topaddr - maxaddr  + 500;
	else if (topaddr < 1100)
		pos = (topaddr - 100) / 10 + 100;
	else if (topaddr >= maxaddr - 1100)
		pos = (topaddr - maxaddr + 1100) / 10 + 300;
	else
		pos = 250;

	int sz = nlines > 20 ? 20 : nlines;
	sb.SetScrollbar(pos, 20, 500, 20);
}

void DisList::MoveView(wxScrollEvent &ev)
{
	int pos = ev.GetPosition();

	if (pos < 100)
		topaddr = pos;
	else if (pos >= 400)
		topaddr = maxaddr + pos - 500;
	else if (ev.GetEventType() == wxEVT_SCROLL_LINEUP)
	{
		topaddr -= back_size;
		MoveSB();
	}
	else if (ev.GetEventType() == wxEVT_SCROLL_LINEDOWN)
	{
		topaddr = addrs[1];
		MoveSB();
	}
	else if (ev.GetEventType() == wxEVT_SCROLL_PAGEUP)
	{
		topaddr -= (nlines - 2) * back_size;
		MoveSB();
	}
	else if (ev.GetEventType() == wxEVT_SCROLL_PAGEDOWN)
	{
		topaddr = addrs[nlines - 2];
		MoveSB();
	}
	else if (ev.GetEventType() == wxEVT_SCROLL_THUMBRELEASE)
	{
		if (pos <= 200)
			topaddr = (pos - 100) * 10 + 100;
		else if (pos >= 300)
			topaddr = (pos - 300) * 10 + maxaddr - 1100;
		else
			// 200 .. 300 -> 1100 .. maxaddr - 1100
			topaddr = (pos - 200) * ((maxaddr - 2200) / 100) + 1100;

		MoveSB();
	} // ignore THUMBTRACK and CHANGED

	RefillNeeded();
}

// generate event; due to ProcessEvent()'s nature, it will return only
// when refill complete
void DisList::RefillNeeded()
{
	wxCommandEvent ev(EVT_REFILL_NEEDED, GetId());
	ev.SetEventObject(this);
	GetEventHandler()->ProcessEvent(ev);
}

// called by parent's refill handler or any other time strings have changed
void DisList::Refill()
{
	MoveSB();
	wxString val;

	for (int i = 0; i < nlines && i < strings.size(); i++)
	{
		val += strings[i];
		val += wxT('\n');
	}

	tc.SetValue(val);
	SetSel();
}

// on resize, recompute shown lines and refill if necessary
void DisList::Resize(wxSizeEvent &ev)
{
	if (!didinit) // prevent crash on win32
		return;

	wxSize sz = GetSize();
	int sbw = sb.GetSize().GetWidth();
	sz.SetWidth(sz.GetWidth() - sbw);
	sb.Move(sz.GetWidth(), 0);
	sb.SetSize(sbw, sz.GetHeight());
	nlines = (sz.GetHeight() + lineheight - 1) / lineheight;
	wxString val;
	tc.SetSize(sz.GetWidth(), (nlines + 1) * lineheight + extraheight);

	if (nlines > strings.size())
		RefillNeeded();
	else
	{
		for (int i = 0; i < nlines && i < strings.size(); i++)
		{
			val += strings[i];
			val += wxT('\n');
		}

		tc.SetValue(val);
		SetSel();
	}
}

// highlight selected line, if visible
void DisList::SetSel()
{
	tc.SetSelection(0, 0);

	if (!issel)
		return;

	if (nlines > addrs.size() || (u32)addrs[0] > seladdr ||
	        (u32)addrs[nlines - 1] <= seladdr)
		return;

	for (int i = 0, start = 0; i < nlines; i++)
	{
		if ((u32)addrs[i + 1] > seladdr)
		{
			int end = start + strings[i].size() + 1;
			// on win32, wx helpfully inserts a CR before every LF
			// it also doesn't highlight the whole line, but that's
			// not critical
#ifdef __WXMSW__
			start += i;
			end += i + 1;
#endif
			tc.SetSelection(start, start + strings[i].size() + 1);
			return;
		}

		start += strings[i].size() + 1;
	}
}

void DisList::SetSel(u32 addr)
{
	seladdr = addr;
	issel = true;

	if (addrs.size() < 4 || addrs.size() < nlines ||
	        topaddr > addr || (u32)addrs[addrs.size() - 4] < addr)
	{
		topaddr = addr;
		strings.clear();
		addrs.clear();
		RefillNeeded();
	}
	else
		SetSel();
}

BEGIN_EVENT_TABLE(DisList, wxPanel)
	EVT_SIZE(DisList::Resize)
	EVT_SCROLL(DisList::MoveView)
END_EVENT_TABLE()

DEFINE_EVENT_TYPE(EVT_REFILL_NEEDED)

IMPLEMENT_DYNAMIC_CLASS(MemView, wxPanel)

MemView::MemView() :
	wxPanel(), disp(), sb(), didinit(false), nlines(0), selnib(-1),
	topaddr(0), addrlab(0)
{
}

void MemView::Refit()
{
	addrlen = maxaddr > 0xffff ? 8 : 4;

	if (!didinit)
	{
		disp.Create(this, wxID_ANY, wxPoint(0, 0), wxDefaultSize,
		            wxBORDER_NONE | wxWANTS_CHARS | wxTAB_TRAVERSAL);
		disp.Connect(wxEVT_PAINT, wxPaintEventHandler(MemView::RepaintEv),
		             NULL, this);
		disp.Connect(wxEVT_LEFT_DOWN, wxMouseEventHandler(MemView::MouseEvent),
		             NULL, this);
		disp.Connect(wxEVT_MOTION, wxMouseEventHandler(MemView::MouseEvent),
		             NULL, this);
		disp.Connect(wxEVT_LEFT_UP, wxMouseEventHandler(MemView::MouseEvent),
		             NULL, this);
		disp.Connect(wxEVT_CHAR, wxKeyEventHandler(MemView::KeyEvent),
		             NULL, this);
		disp.SetBackgroundColour(wxSystemSettings::GetColour(wxSYS_COLOUR_LISTBOX));
		sb.Create(this, wxID_ANY, wxDefaultPosition, wxDefaultSize,
		          wxSB_VERTICAL);
		sb.SetScrollbar(0, 15, 500, 15);
	}

	wxClientDC dc(&disp);
	// doesn't seem to inherit font properly
	dc.SetFont(GetFont());
	dc.GetTextExtent(wxT('M'), &charwidth, &charheight);

	if (!didinit)
	{
		// using 2-phase init doesn't seem to work
		caret = new wxCaret(&disp, charwidth, charheight);
		caret->Hide();
		disp.SetCaret(caret);
		didinit = true;
	}

	wxSize sz(charwidth * (69 + addrlen) + sb.GetBestSize().GetWidth(),
	          charheight * 15);
	sz = sz + GetSize() - GetClientSize();
	SetMinSize(sz);
	SetSize(sz);
}

void MemView::MouseEvent(wxMouseEvent &ev)
{
	if (ev.GetEventType() == wxEVT_MOTION && !ev.LeftIsDown())
		return;

	int x = ev.GetX() / charwidth, y = ev.GetY() / charheight;
	x -= addrlen + 3;

	if (x < 0 || y < 0 || y > nlines)
		return;

	int word, nib;
	int nnib = 2 << fmt, nword = 16 >> fmt;
	int preasc = (nnib + 1) * nword + 2;
	isasc = x >= preasc;

	if (isasc)
	{
		word = (x - preasc) * 2 / nnib;
		nib = (x - preasc) * 2 % nnib;
	}
	else
	{
		word = x / (nnib + 1);
		nib = x % (nnib + 1);
		nib = nnib - nib - 1;
	}

	if (nib < 0 || word >= nword)
		return;

	seladdr = topaddr + y * 16;
	selnib = word * nnib + nib;
	Show(seladdr);
}

void MemView::ShowCaret()
{
	if (seladdr < topaddr || seladdr >= topaddr + nlines * 16)
		selnib = -1;

	if (selnib < 0)
	{
		while (caret->IsVisible())
			caret->Hide();

		if (addrlab)
			addrlab->SetLabel(wxEmptyString);

		return;
	}

	if (addrlab)
	{
		wxString lab;
		u32 addr = seladdr + selnib / 2;

		if (!isasc)
			addr &= ~((1 << fmt) - 1);

		lab.Printf(addrlen == 8 ? wxT("0x%08X") : wxT("0x%04X"), addr);
		addrlab->SetLabel(lab);
	}

	int y = (seladdr - topaddr) / 16;
	int x = addrlen + 3;
	int nnib = 2 << fmt, nword = 16 >> fmt;

	if (isasc)
		x += (nnib + 1) * nword + 2 + selnib / 2;
	else
		x += (nnib + 1) * (selnib / nnib) + nnib - selnib % nnib - 1;

	caret->Move(x * charwidth, y * charheight);

	while (!caret->IsVisible())
		caret->Show();

	disp.SetFocus();
}

void MemView::KeyEvent(wxKeyEvent &ev)
{
	u32 k = ev.GetKeyCode();
	int nnib = 2 << fmt;

	switch (k)
	{
	case WXK_RIGHT:
	case WXK_NUMPAD_RIGHT:
		if (isasc)
			selnib += 2;
		else if (ev.GetModifiers() == wxMOD_SHIFT)
			selnib += 2 << fmt;
		else if (!(selnib % nnib))
			selnib += nnib + nnib - 1;
		else
			selnib--;

		if (selnib >= 32)
		{
			if (seladdr == maxaddr - 16)
				selnib = 32 - nnib;
			else
			{
				selnib -= 32;
				seladdr += 16;
			}
		}

		break;

	case WXK_LEFT:
	case WXK_NUMPAD_LEFT:
		if (isasc)
			selnib -= 2;
		else if (ev.GetModifiers() == wxMOD_SHIFT)
			selnib -= 2 << fmt;
		else if (!(++selnib % nnib))
			selnib -= nnib * 2;

		if (selnib < 0)
		{
			if (!seladdr)
				selnib = nnib - 1;
			else
			{
				selnib += 32;
				seladdr -= 16;
			}
		}

		break;

	case WXK_DOWN:
	case WXK_NUMPAD_DOWN:
		if (seladdr < maxaddr - 16)
			seladdr += 16;

		break;

	case WXK_UP:
	case WXK_NUMPAD_UP:
		if (seladdr > 0)
			seladdr -= 16;

		break;

	default:
		if (k > 0x7f ||
		        (isasc && !isprint(k)) || (!isasc && !isxdigit(k)))
		{
			ev.Skip();
			return;
		}

		// location in data array
		int wno = (seladdr - topaddr) / 4 + selnib / 8;
		int bno = (selnib % 8) / 2;
		int nibno = selnib % 2;

		// now that selnib/seladdr isn't needed any more, advance pointer
		if (isasc)
			selnib += 2;
		else if (!(selnib % nnib))
			selnib += nnib + nnib - 1;
		else
			selnib--;

		if (selnib >= 32)
		{
			if (seladdr == maxaddr - 16)
				selnib = 32 - nnib;
			else
			{
				selnib -= 32;
				seladdr += 16;
			}
		}

		u32 mask, val;

		if (isasc)
		{
			mask = 0xff << bno * 8;
			val = k << bno * 8;
		}
		else
		{
			mask = 0xf << bno * 8 + nibno * 4;
			val = isdigit(k) ? k - '0' : tolower(k) + 10 - 'a';
			val <<= bno * 8 + nibno * 4;
		}

		if ((words[wno] & mask) == val)
			break;

		words[wno] = words[wno] & ~mask | val;
		writeaddr = topaddr + 4 * wno;
		val = words[wno];

		switch (fmt)
		{
		case 0:
			writeval = (val >> bno * 8) & 0xff;
			writeaddr += bno;
			break;

		case 1:
			writeval = (val >> (bno / 2) * 16) & 0xffff;
			writeaddr += bno & ~1;
			break;

		case 2:
			writeval = val;
			break;
		}

		// write value; this will not return until value has been written
		wxCommandEvent ev(EVT_WRITEVAL, GetId());
		ev.SetEventObject(this);
		GetEventHandler()->ProcessEvent(ev);
		// now refresh whole screen.  Really need to make this more
		// efficient some day
		Repaint();
	}

	Show(seladdr);
}

void MemView::MoveSB()
{
	int pos;

	if (topaddr / 16 <= 100)
		pos = topaddr / 16;
	else if (topaddr / 16 >= maxaddr / 16 - 100)
		pos = topaddr / 16 - maxaddr / 16 + 500;
	else if (topaddr / 16 < 1100)
		pos = (topaddr / 16 - 100) / 10 + 100;
	else if (topaddr / 16 >= maxaddr / 16 - 1100)
		pos = (topaddr / 16 - maxaddr / 16 + 1100) / 10 + 300;
	else
		pos = 250;

	int sz = nlines > 20 ? 20 : nlines;
	sb.SetScrollbar(pos, 20, 500, 20);
}

void MemView::MoveView(wxScrollEvent &ev)
{
	int pos = ev.GetPosition();

	if (pos < 100)
		topaddr = pos * 16;
	else if (pos >= 400)
		topaddr = maxaddr + (pos - 500) * 16;
	else if (ev.GetEventType() == wxEVT_SCROLL_LINEUP)
	{
		topaddr -= 16;
		MoveSB();
	}
	else if (ev.GetEventType() == wxEVT_SCROLL_LINEDOWN)
	{
		topaddr += 16;
		MoveSB();
	}
	else if (ev.GetEventType() == wxEVT_SCROLL_PAGEUP)
	{
		topaddr -= (nlines - 2) * 16;
		MoveSB();
	}
	else if (ev.GetEventType() == wxEVT_SCROLL_PAGEDOWN)
	{
		topaddr += (nlines - 2) * 16;
		MoveSB();
	}
	else if (ev.GetEventType() == wxEVT_SCROLL_THUMBRELEASE)
	{
		if (pos <= 200)
			topaddr = ((pos - 100) * 10 + 100) * 16;
		else if (pos >= 300)
			topaddr = ((pos - 300) * 10 - 1100) * 16 + maxaddr;
		else
			topaddr = ((pos - 200) * ((maxaddr / 16 - 2200) / 100) + 1100) * 16;

		MoveSB();
	} // ignore THUMBTRACK and CHANGED

	RefillNeeded();
}

// generate event; due to ProcessEvent()'s nature, it will return only
// when refill complete
void MemView::RefillNeeded()
{
	wxCommandEvent ev(EVT_REFILL_NEEDED, GetId());
	ev.SetEventObject(this);
	GetEventHandler()->ProcessEvent(ev);
}

// FIXME:  following 3 repainters do not work as intended: there is
// more flickering than necessary and yet still garbage on screen sometimes
// (probably in part from wxCaret)

// called by parent's refill handler or any other time strings have changed
void MemView::Refill()
{
	MoveSB();
	wxClientDC dc(&disp);
	dc.Clear();
	Refill(dc);
	ShowCaret();
}

void MemView::Repaint()
{
	wxClientDC dc(&disp);
	dc.SetBackgroundMode(wxSOLID);
	Refill(dc);
}

void MemView::RepaintEv(wxPaintEvent &ev)
{
	wxPaintDC dc(&disp);
	dc.SetBackgroundMode(wxSOLID);
	Refill(dc);
	ShowCaret();
}

void MemView::Refill(wxDC &dc)
{
	// don't want caret drawing at same time due to timer event
	wxCaretSuspend cs(&disp);
	// doesn't seem to inherit font properly
	dc.SetFont(GetFont());

	for (int i = 0; i < nlines && i < words.size() / 4; i++)
	{
		wxString line, word;
		line.Printf(maxaddr > 0xffff ? wxT("%08X   ") : wxT("%04X   "), topaddr + i * 16);

		for (int j = 0; j < 4; j++)
		{
			u32 v = words[i * 4 + j];

			switch (fmt)
			{
			case 0:
				word.Printf(wxT("%02X %02X %02X %02X "),
				            v & 0xff, (v >> 8) & 0xff, (v >> 16) & 0xff,
				            (v >> 24) & 0xff);
				break;

			case 1:
				word.Printf(wxT("%04X %04X "), v & 0xffff, (v >> 16) & 0xffff);
				break;

			case 2:
				word.Printf(wxT("%08X "), v);
				break;
			}

			line.append(word);
		}

		line.append(wxT("  "));

		for (int j = 0; j < 4; j++)
		{
			u32 v = words[i * 4 + j];
#define appendc(c) line.append(isascii((c) & 0xff) && isprint((c) & 0xff) ? \
    (wxChar)((c) & 0xff) : wxT('.'))
			appendc(v);
			appendc(v >> 8);
			appendc(v >> 16);
			appendc(v >> 24);
		}

		dc.DrawText(line, 0, i * charheight);
	}

	int lloc = charwidth * ((addrlen + 1) * 2 + 1) / 2;
	dc.DrawLine(lloc, 0, lloc, nlines * charheight);
	lloc = charwidth * (2 * (addrlen + 3 + 32 + 4 +
	                         (fmt == 0 ? 3 * 4 : fmt == 1 ? 4 : 0)) + 1) / 2;
	dc.DrawLine(lloc, 0, lloc, nlines * charheight);
}

// on resize, recompute shown lines and refill if necessary
void MemView::Resize(wxSizeEvent &ev)
{
	if (!didinit) // prevent crash on win32
		return;

	wxSize sz = GetClientSize();
	int sbw = sb.GetSize().GetWidth();
	sz.SetWidth(sz.GetWidth() - sbw);
	sb.Move(sz.GetWidth(), 0);
	sb.SetSize(sbw, sz.GetHeight());
	nlines = (sz.GetHeight() + charheight - 1) / charheight;
	wxString val;
	disp.SetSize(sz.GetWidth(), (nlines + 1) * charheight);

	if (nlines > words.size() / 4)
	{
		if (topaddr + nlines * 16 > maxaddr)
			topaddr = maxaddr - nlines * 16 + 1;

		RefillNeeded();
	}
	else
		Refill();
}

void MemView::Show(u32 addr, bool force_update)
{
	if (addr < topaddr || addr >= topaddr + (nlines - 1) * 16)
	{
		// align to nearest 16-byte block
		// note that mfc interface only aligns to nearest (1<<fmt)-byte
		u32 newtopaddr = addr & ~0xf;

		if (newtopaddr + nlines * 16 > maxaddr)
			newtopaddr = maxaddr - nlines * 16 + 1;

		force_update = newtopaddr != topaddr;
		topaddr = newtopaddr;
	}

	if (force_update)
	{
		words.clear();
		RefillNeeded();
	}
	else
		ShowCaret();
}

u32 MemView::GetAddr()
{
	if (selnib < 0)
		return topaddr;
	else
		return seladdr + (selnib / 2 & ~((1 << fmt) - 1));
}

BEGIN_EVENT_TABLE(MemView, wxPanel)
	EVT_SIZE(MemView::Resize)
	EVT_SCROLL(MemView::MoveView)
END_EVENT_TABLE()

DEFINE_EVENT_TYPE(EVT_WRITEVAL)

ColorView::ColorView(wxWindow* parent, wxWindowID id)
// default for MSW appears to be BORDER_SUNKEN
	: wxControl(parent, id, wxDefaultPosition, wxDefaultSize, wxBORDER_NONE),
	  r(0), g(0), b(0)
{
	wxBoxSizer* sz = new wxBoxSizer(wxHORIZONTAL);
	cp = new wxPanel(this, wxID_ANY, wxDefaultPosition, wxSize(75, 75),
	                 wxBORDER_SUNKEN);
	sz->Add(cp);
	wxGridSizer* gs = new wxGridSizer(2);
	wxStaticText* lab = new wxStaticText(this, wxID_ANY, _("R:"));
	gs->Add(lab, 0, wxALL | wxALIGN_CENTER_VERTICAL, 5);
	rt = new wxStaticText(this, wxID_ANY, wxT("255"), wxDefaultPosition,
	                      wxDefaultSize, wxST_NO_AUTORESIZE);
	gs->Add(rt, 0, wxALL | wxALIGN_CENTER_VERTICAL, 5);
	lab = new wxStaticText(this, wxID_ANY, _("G:"));
	gs->Add(lab, 0, wxALL | wxALIGN_CENTER_VERTICAL, 5);
	gt = new wxStaticText(this, wxID_ANY, wxT("255"), wxDefaultPosition,
	                      wxDefaultSize, wxST_NO_AUTORESIZE);
	gs->Add(gt, 0, wxALL | wxALIGN_CENTER_VERTICAL, 5);
	lab = new wxStaticText(this, wxID_ANY, _("B:"));
	gs->Add(lab, 0, wxALL | wxALIGN_CENTER_VERTICAL, 5);
	bt = new wxStaticText(this, wxID_ANY, wxT("255"), wxDefaultPosition,
	                      wxDefaultSize, wxST_NO_AUTORESIZE);
	gs->Add(bt, 0, wxALL | wxALIGN_CENTER_VERTICAL, 5);
	sz->Add(gs);
	sz->Layout();
	SetSizerAndFit(sz);
	SetRGB(-1, -1, -1);
}

void ColorView::SetRGB(int r, int g, int b)
{
	if (r == -1 || g == -1 || b == -1)
	{
		cp->SetBackgroundColour(wxNullColour);
		rt->SetLabel(wxT(""));
		gt->SetLabel(wxT(""));
		bt->SetLabel(wxT(""));
		return;
	}

	cp->SetBackgroundColour(wxColour(r, g, b));
	wxString s;
	// FIXME: make shift an option; currently hard-coded to rgb555
	s.Printf(wxT("%d"), r >> 3);
	rt->SetLabel(s);
	s.Printf(wxT("%d"), g >> 3);
	gt->SetLabel(s);
	s.Printf(wxT("%d"), b >> 3);
	bt->SetLabel(s);
}

IMPLEMENT_DYNAMIC_CLASS(PixView, wxPanel);

BEGIN_EVENT_TABLE(PixView, wxPanel)
	EVT_PAINT(PixView::Redraw)
	EVT_LEFT_UP(PixView::SelPoint)
END_EVENT_TABLE()

bool PixView::InitBMP(int w, int h, ColorView* cv)
{
	im = wxImage(w, h);
	bm = new wxBitmap(im);

	if (!bm)
		return false;

	selx = sely = -1;
	cview = cv;
	return true;
}

void PixView::SetData(const unsigned char* data, int stride, int x, int y)
{
	if (!bm)
		return;

	ox = x;
	oy = y;

	if (!data)
	{
		im.SetRGB(wxRect(0, 0, im.GetWidth(), im.GetHeight()), 0, 0, 0);
		selx = sely = -1;
	}
	else
	{
		data += (y * stride + x) * 3;

		for (y = 0; y < im.GetHeight(); y++)
		{
			for (x = 0; x < im.GetWidth(); x++)
			{
				im.SetRGB(x, y, *data, data[1], data[2]);
				data += 3;
			}

			data += 3 * (stride - x);
		}
	}

	delete bm;
	bm = new wxBitmap(im);

	if (selx >= 0 && cview)
		cview->SetRGB(im.GetRed(selx, sely), im.GetGreen(selx, sely),
		              im.GetBlue(selx, sely));
	else if (cview)
		cview->SetRGB(-1, -1, -1);

	Refresh();
}

void PixView::SetSel(int x, int y, bool desel_cview_update)
{
	if (x >= ox && y >= oy && x - ox < im.GetWidth() && y - oy < im.GetHeight())
	{
		int oselx = selx, osely = sely;
		selx = x - ox;
		sely = y - oy;

		if (selx != oselx || sely != osely)
		{
			if (cview)
				cview->SetRGB(im.GetRed(selx, sely), im.GetGreen(selx, sely),
				              im.GetBlue(selx, sely));

			Refresh();
		}
	}
	else
	{
		bool r = selx >= 0 && sely >= 0;
		selx = sely = -1;

		if (r)
		{
			if (cview && desel_cview_update)
				cview->SetRGB(-1, -1, -1);

			Refresh();
		}
	}
}

void PixView::Redraw(wxPaintEvent &ev)
{
	if (!bm)
		return;

	wxPaintDC dc(this);
	double sx, sy;
	int w, h;
	GetClientSize(&w, &h);
	sx = (double)w / im.GetWidth();
	sy = (double)h / im.GetHeight();
	dc.SetUserScale(sx, sy);
	dc.DrawBitmap(*bm, 0, 0, false);
	dc.SetUserScale(sx / 8, sy / 8);
	// grid color is hard-coded to gray
	dc.SetPen(*wxGREY_PEN);

	// grid is only on top/left, just like win32 port
	for (int y = 0; y < im.GetHeight(); y++)
		dc.DrawLine(0, y * 8, im.GetWidth() * 8, y * 8);

	for (int x = 0; x < im.GetWidth(); x++)
		dc.DrawLine(x * 8, 0, x * 8, im.GetHeight() * 8);

	if (selx >= 0)
	{
		// sel color is hard-coded to red
		dc.SetPen(*wxRED_PEN);
		dc.SetBrush(*wxTRANSPARENT_BRUSH);
		dc.DrawRectangle(selx * 8, sely * 8, 8, 8);
	}
}

void PixView::SelPoint(wxMouseEvent &ev)
{
	if (!bm)
		return;

	int w, h;
	GetClientSize(&w, &h);
	wxPoint p = ev.GetPosition();

	if (p.x < 0 || p.x >= w || p.y < 0 || p.y >= h)
	{
		bool r = selx >= 0 && sely >= 0;
		selx = sely = -1;

		if (r)
		{
			Refresh();

			if (cview)
				cview->SetRGB(-1, -1, -1);
		}

		return;
	}

	int oselx = selx, osely = sely;
	selx = p.x * im.GetWidth() / w;
	sely = p.y * im.GetHeight() / h;

	if (selx != oselx || sely != osely)
	{
		Refresh();

		if (cview)
			cview->SetRGB(im.GetRed(selx, sely), im.GetGreen(selx, sely),
			              im.GetBlue(selx, sely));
	}
}

IMPLEMENT_DYNAMIC_CLASS(PixViewEvt, PixView)
void PixViewEvt::SetData(const unsigned char* data, int stride, int x,
                         int y)
{
	PixView::SetData(data, stride, x, y);

	if (selx >= 0 && sely >= 0)
		click();
}

void PixViewEvt::SelPoint(wxMouseEvent &ev)
{
	PixView::SelPoint(ev);
	click();
}

void PixViewEvt::click()
{
	wxMouseEvent ev;
	ev.SetEventType(EVT_COMMAND_GFX_CLICK);
	ev.ResumePropagation(wxEVENT_PROPAGATE_MAX);
	ev.SetEventObject(this);
	ev.SetId(GetId());
	//ev.SetX(selx);
	ev.m_x = selx;
	//ev.SetY(sely);
	ev.m_y = sely;
	GetEventHandler()->ProcessEvent(ev);
}

IMPLEMENT_DYNAMIC_CLASS(GfxPanel, wxPanel)
void GfxPanel::DrawBitmap(wxPaintEvent &ev)
{
	if (!bm)
		return;

	wxPaintDC dc(this);
	wxSize sz = GetClientSize();
	double scalex = (double)sz.GetWidth() / bmw;
	double scaley = (double)sz.GetHeight() / bmh;
	dc.SetUserScale(scalex, scaley);
	dc.DrawBitmap(*bm, 0, 0);

	if (selx >= 0)
	{
		if (selx > bmw - 4 || sely > bmh - 4)
			pv->SetData(NULL, 0, 0, 0);
		else
			pv->SetData(im->GetData(), im->GetWidth(), selx - 4, sely - 4);
	}
}

void GfxPanel::Click(wxMouseEvent &ev)
{
	DoSel(ev, ev.GetEventType() == wxEVT_LEFT_DOWN);
}

void GfxPanel::MouseMove(wxMouseEvent &ev)
{
	if (!ev.LeftIsDown())
	{
		ev.Skip();
		return;
	}

	DoSel(ev);
}

void GfxPanel::DoSel(wxMouseEvent &ev, bool force)
{
	if (!bm)
		return;

	int x = ev.GetX(), y = ev.GetY();

	if (x < 0 || y < 0)
		return;

	wxSize sz = GetClientSize();

	if (x > sz.GetWidth() || y > sz.GetHeight())
		return;

	x = x * bmw / sz.GetWidth();
	y = y * bmh / sz.GetHeight();
	wxMouseEvent ev2(ev);
	ev2.SetEventType(EVT_COMMAND_GFX_CLICK);
	ev2.ResumePropagation(wxEVENT_PROPAGATE_MAX);
	ev2.SetEventObject(this);
	ev2.SetId(GetId());
	//ev2.SetX(x);
	ev2.m_x = x;
	//ev2.SetY(y);
	ev2.m_y = y;
	GetEventHandler()->ProcessEvent(ev2);

	if (x < 4)
		x = 4;
	else if (x > bmw - 4)
		x = bmw - 4;

	if (y < 4)
		y = 4;
	else if (y > bmh - 4)
		y = bmh - 4;

	if (force || selx != x || sely != y)
	{
		selx = x;
		sely = y;
		pv->SetData(im->GetData(), im->GetWidth(), selx - 4, sely - 4);
	}
}

DEFINE_EVENT_TYPE(EVT_COMMAND_GFX_CLICK)

BEGIN_EVENT_TABLE(GfxPanel, wxPanel)
	EVT_PAINT(GfxPanel::DrawBitmap)
	EVT_LEFT_DOWN(GfxPanel::Click)
	EVT_LEFT_UP(GfxPanel::Click)
	EVT_MOTION(GfxPanel::MouseMove)
END_EVENT_TABLE()

GfxViewer::GfxViewer(const wxChar* dname, int maxw, int maxh) :
	Viewer(dname), image(maxw, maxh)
{
	gv = XRCCTRL(*this, "GfxView", GfxPanel);

	if (!gv || !(gvs = dynamic_cast<wxScrolledWindow*>(gv->GetParent())))
		baddialog();

	gvs->SetMinSize(gvs->GetSize());
	gvs->SetScrollRate(1, 1);
	gv->SetSize(maxw, maxh);
	gvs->SetVirtualSize(maxw, maxh);
	gv->im = &image;
	gv->bmw = maxw;
	gv->bmh = maxh;
	ColorView* cv;
	colorctrl(cv, "Color");
	pixview(gv->pv, "Zoom", 8, 8, cv);
	str = XRCCTRL(*this, "Stretch", wxCheckBox);

	if (!str)
		baddialog();
}

void GfxViewer::ChangeBMP()
{
	delete gv->bm;
	gv->bm = new wxBitmap(image);
	gv->Refresh();
}

void GfxViewer::BMPSize(int w, int h)
{
	if (gv->bmw != w || gv->bmh != h)
	{
		gv->bmw = w;
		gv->bmh = h;

		if (!str->GetValue())
		{
			gv->SetSize(w, h);
			gvs->SetVirtualSize(gv->GetSize());
		}
	}
}

void GfxViewer::StretchTog(wxCommandEvent &ev)
{
	wxSize sz;

	if (str->GetValue())
	{
		// first time to remove scrollbars
		gvs->SetVirtualSize(gvs->GetClientSize());
		// second time to expand to outer edges
		sz = gvs->GetClientSize();
	}
	else
		sz = wxSize(gv->bmw, gv->bmh);

	gv->SetSize(sz);
	gvs->SetVirtualSize(sz);
}

void GfxViewer::SaveBMP(wxCommandEvent &ev)
{
	GameArea* panel = wxGetApp().frame->GetPanel();
	bmp_save_dir = wxGetApp().frame->GetGamePath(gopts.scrshot_dir);
	// no attempt is made here to translate the dialog type name
	// it's just a suggested name, anyway
	wxString def_name = panel->game_name() + wxT('-') + dname;
	def_name.resize(def_name.size() - 6); // strlen("Viewer")

	if (captureFormat)
		def_name += wxT(".bmp");
	else
		def_name += wxT(".png");

	wxFileDialog dlg(GetGrandParent(), _("Select output file"), bmp_save_dir, def_name,
	                 _("PNG images|*.png|BMP images|*.bmp"), wxFD_SAVE | wxFD_OVERWRITE_PROMPT);
	dlg.SetFilterIndex(captureFormat);
	int ret = dlg.ShowModal();
	bmp_save_dir = dlg.GetDirectory();

	if (ret != wxID_OK)
		return;

	wxBitmap obmp = gv->bm->GetSubBitmap(wxRect(0, 0, gv->bmh, gv->bmh));
	wxString fn = dlg.GetPath();
	wxBitmapType fmt = dlg.GetFilterIndex() ? wxBITMAP_TYPE_BMP : wxBITMAP_TYPE_PNG;

	if (fn.size() > 4)
	{
		if (wxString(fn.substr(fn.size() - 4)).IsSameAs(wxT(".bmp"), false))
			fmt = wxBITMAP_TYPE_BMP;
		else if (wxString(fn.substr(fn.size() - 4)).IsSameAs(wxT(".png"), false))
			fmt = wxBITMAP_TYPE_PNG;
	}

	obmp.SaveFile(fn, fmt);
}

void GfxViewer::RefreshEv(wxCommandEvent &ev)
{
	Update();
}

wxString GfxViewer::bmp_save_dir = wxEmptyString;

BEGIN_EVENT_TABLE(GfxViewer, Viewer)
	EVT_CHECKBOX(XRCID("Stretch"), GfxViewer::StretchTog)
	EVT_BUTTON(XRCID("Refresh"), GfxViewer::RefreshEv)
	EVT_BUTTON(XRCID("Save"), GfxViewer::SaveBMP)
END_EVENT_TABLE()

IMPLEMENT_DYNAMIC_CLASS(DispCheckBox, wxCheckBox)

BEGIN_EVENT_TABLE(DispCheckBox, wxCheckBox)
	EVT_LEFT_DOWN(DispCheckBox::MouseEvent)
	EVT_LEFT_UP(DispCheckBox::MouseEvent)
END_EVENT_TABLE()
}
